/*
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.zz.common;

import com.zz.constants.EsignRequestType;
import com.zz.exception.EsignDemoException;
import org.apache.http.*;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.*;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * @author 澄泓
 * @description Http请求 辅助类
 * @since JDK1.7
 */
public class EsignHttpCfgHelper {

    private static Logger LOGGER = LoggerFactory.getLogger(EsignHttpCfgHelper.class);
    /**
     * 超时时间,默认15000毫秒
     */
    private static int MAX_TIMEOUT = 15000;
    /**
     * 请求池最大连接数,默认100个
     */
    private static int MAX_TOTAL = 100;
    /**
     * 单域名最大的连接数,默认50个
     */
    private static int ROUTE_MAX_TOTAL = 50;
    /**
     * 请求失败重试次数,默认3次
     */
    private static int MAX_RETRY = 3;
    /**
     * 是否需要域名校验,默认不需要校验
     */
    private static boolean SSL_VERIFY = false;

    /**
     * 正向代理IP
     */
    private static String PROXY_IP;
    /**
     * 正向代理端口,默认8888
     */
    private static int PROXY_PORT = 8888;
    /**
     * 代理协议,默认http
     */
    private static String PROXY_AGREEMENT = "http";

    /**
     * 是否开启代理，默认false
     */
    private static boolean OPEN_PROXY = false;

    /**
     * 代理服务器用户名
     */
    private static String PROXY_USERNAME = "";

    /**
     * 代理服务器密码
     */
    private static String PROXY_PASSWORD = "";


    private static PoolingHttpClientConnectionManager connMgr; //连接池
    private static HttpRequestRetryHandler retryHandler; //重试机制

    private static CloseableHttpClient httpClient = null;

    public static int getMaxTimeout() {
        return MAX_TIMEOUT;
    }

    public static void setMaxTimeout(int maxTimeout) {
        MAX_TIMEOUT = maxTimeout;
    }

    public static int getMaxTotal() {
        return MAX_TOTAL;
    }

    public static void setMaxTotal(int maxTotal) {
        MAX_TOTAL = maxTotal;
    }

    public static int getRouteMaxTotal() {
        return ROUTE_MAX_TOTAL;
    }

    public static void setRouteMaxTotal(int routeMaxTotal) {
        ROUTE_MAX_TOTAL = routeMaxTotal;
    }

    public static int getMaxRetry() {
        return MAX_RETRY;
    }

    public static void setMaxRetry(int maxRetry) {
        MAX_RETRY = maxRetry;
    }

    public static boolean isSslVerify() {
        return SSL_VERIFY;
    }

    public static void setSslVerify(boolean sslVerify) {
        SSL_VERIFY = sslVerify;
    }

    public static String getProxyIp() {
        return PROXY_IP;
    }

    public static void setProxyIp(String proxyIp) {
        PROXY_IP = proxyIp;
    }

    public static int getProxyPort() {
        return PROXY_PORT;
    }

    public static void setProxyPort(int proxyPort) {
        PROXY_PORT = proxyPort;
    }

    public static String getProxyAgreement() {
        return PROXY_AGREEMENT;
    }

    public static void setProxyAgreement(String proxyAgreement) {
        PROXY_AGREEMENT = proxyAgreement;
    }

    public static boolean getOpenProxy() {
        return OPEN_PROXY;
    }

    public static void setOpenProxy(boolean openProxy) {
        OPEN_PROXY = openProxy;
    }

    public static String getProxyUsername() {
        return PROXY_USERNAME;
    }

    public static void setProxyUserame(String proxyUsername) {
        PROXY_USERNAME = proxyUsername;
    }

    public static String getProxyPassword() {
        return PROXY_PASSWORD;
    }

    public static void setProxyPassword(String proxyPassword) {
        PROXY_PASSWORD = proxyPassword;
    }


    /**
     * 不允许外部创建实例
     */
    private EsignHttpCfgHelper() {
    }

    //------------------------------公有方法start--------------------------------------------


    /**
     * @param reqType {@link EsignRequestType} 请求类型  GET、 POST 、 DELETE 、 PUT
     * @param httpUrl {@link String} 请求目标地址
     * @param headers {@link Map} 请求头
     * @param param   {@link Object} 参数
     * @return
     * @throws EsignDemoException
     * @description 发起HTTP / HTTPS 请求
     * @author 澄泓
     */
    public static EsignHttpResponse sendHttp(EsignRequestType reqType, String httpUrl, Map<String, String> headers, Object param, boolean debug)
            throws EsignDemoException {
        HttpRequestBase reqBase = null;
        if (httpUrl.startsWith("http")) {
            reqBase = reqType.getHttpType(httpUrl);
        } else {
            throw new EsignDemoException("请求url地址格式错误");
        }
        if (debug) {
            //LOGGER.info("请求头:{}", headers + "");
            //LOGGER.info("请求参数{}", param + "");
            //LOGGER.info("请求地址:{}请求方式:{}", reqBase.getURI(), reqType + "");
        }
        //请求方法不是GET或者DELETE时传入body体，否则不传入。
        String[] methods = {"DELETE", "GET"};
        if (param instanceof String && Arrays.binarySearch(methods, reqType.name()) < 0) {//POST或者PUT请求
            ((HttpEntityEnclosingRequest) reqBase).setEntity(
                    new StringEntity(String.valueOf(param), ContentType.create("application/json", "UTF-8")));
        }
        //参数时字节流数组
        else if (param instanceof byte[]) {
            reqBase = reqType.getHttpType(httpUrl);
            byte[] paramBytes = (byte[]) param;
            ((HttpEntityEnclosingRequest) reqBase).setEntity(new ByteArrayEntity(paramBytes));
        }
        //参数是form表单时
        else if (param instanceof List) {
            ((HttpEntityEnclosingRequest) reqBase).setEntity(new UrlEncodedFormEntity((Iterable<? extends NameValuePair>) param));
        }
        httpClient = getHttpClient();
        config(reqBase);

        //设置请求头
        if (headers != null && headers.size() > 0) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                reqBase.setHeader(entry.getKey(), entry.getValue());
            }
        }
        //响应对象
        CloseableHttpResponse res = null;
        //响应内容
        String resCtx = null;
        int status;
        EsignHttpResponse esignHttpResponse = new EsignHttpResponse();
        try {
            //执行请求
            res = httpClient.execute(reqBase);
            status = res.getStatusLine().getStatusCode();

            //获取请求响应对象和响应entity
            HttpEntity httpEntity = res.getEntity();
            if (httpEntity != null) {
                resCtx = EntityUtils.toString(httpEntity, "utf-8");
            }
            if (debug) {
                LOGGER.info("请求接口响应{}", resCtx + "");
                //LOGGER.info("----------------------------end------------------------");
            }
        } catch (NoHttpResponseException e) {
            throw new EsignDemoException("服务器丢失了", e);
        } catch (SSLHandshakeException e) {
            String msg = MessageFormat.format("SSL握手异常", e);
            EsignDemoException ex = new EsignDemoException(msg, e);
            throw ex;
        } catch (UnknownHostException e) {
            EsignDemoException ex = new EsignDemoException("服务器找不到", e);
            ex.initCause(e);
            throw ex;
        } catch (ConnectTimeoutException e) {
            EsignDemoException ex = new EsignDemoException("连接超时", e);
            ex.initCause(e);
            throw ex;
        } catch (SSLException e) {
            EsignDemoException ex = new EsignDemoException("SSL异常", e);
            ex.initCause(e);
            throw ex;
        } catch (ClientProtocolException e) {
            EsignDemoException ex = new EsignDemoException("请求头异常", e);
            ex.initCause(e);
            throw ex;
        } catch (IOException e) {
            EsignDemoException ex = new EsignDemoException("网络请求失败", e);
            ex.initCause(e);
            throw ex;
        } finally {
            if (res != null) {
                try {
                    res.close();
                } catch (IOException e) {
                    EsignDemoException ex = new EsignDemoException("--->>关闭请求响应失败", e);
                    ex.initCause(e);
                    throw ex;
                }
            }
        }
        esignHttpResponse.setStatus(status);
        esignHttpResponse.setBody(resCtx);
        return esignHttpResponse;
    }
    //------------------------------公有方法end----------------------------------------------

    //------------------------------私有方法start--------------------------------------------

    /**
     * @param httpReqBase
     * @description 请求头和超时时间配置
     * @author 澄泓
     */
    private static void config(HttpRequestBase httpReqBase) {
        // 配置请求的超时设置
        RequestConfig.Builder builder = RequestConfig.custom()
                .setConnectionRequestTimeout(MAX_TIMEOUT)
                .setConnectTimeout(MAX_TIMEOUT)
                .setSocketTimeout(MAX_TIMEOUT);
        if (OPEN_PROXY) {
            HttpHost proxy = new HttpHost(PROXY_IP, PROXY_PORT, PROXY_AGREEMENT);
            builder.setProxy(proxy);
        }
        RequestConfig requestConfig = builder.build();
        httpReqBase.setConfig(requestConfig);
    }

    /**
     * @return
     * @description 连接池配置
     * @author 澄泓
     */
    private static void cfgPoolMgr() throws EsignDemoException {
        ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory();
        LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory.getSocketFactory();
        if (!SSL_VERIFY) {
            sslsf = sslConnectionSocketFactory();
        }

        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", plainsf)
                .register("https", sslsf)
                .build();

        //连接池管理器
        connMgr = new PoolingHttpClientConnectionManager(registry);
        //请求池最大连接数
        connMgr.setMaxTotal(MAX_TOTAL);
        //但域名最大的连接数
        connMgr.setDefaultMaxPerRoute(ROUTE_MAX_TOTAL);
    }


    /**
     * @description 设置重试机制
     * @author 澄泓
     */
    private static void cfgRetryHandler() {
        retryHandler = new HttpRequestRetryHandler() {

            @Override
            public boolean retryRequest(IOException e, int excCount, HttpContext ctx) {
                //超过最大重试次数，就放弃
                if (excCount > MAX_RETRY) {
                    return false;
                }
                //服务器丢掉了链接，就重试
                if (e instanceof NoHttpResponseException) {
                    return true;
                }
                //不重试SSL握手异常
                if (e instanceof SSLHandshakeException) {
                    return false;
                }
                //中断
                if (e instanceof InterruptedIOException) {
                    return false;
                }
                //目标服务器不可达
                if (e instanceof UnknownHostException) {
                    return false;
                }
                //连接超时
                //SSL异常
                if (e instanceof SSLException) {
                    return false;
                }

                HttpClientContext clientCtx = HttpClientContext.adapt(ctx);
                HttpRequest req = clientCtx.getRequest();
                //如果是幂等请求，就再次尝试
                if (!(req instanceof HttpEntityEnclosingRequest)) {
                    return true;
                }
                return false;
            }
        };
    }

    /**
     * 忽略域名校验
     */
    private static SSLConnectionSocketFactory sslConnectionSocketFactory() throws EsignDemoException {
        try {
            SSLContext ctx = SSLContext.getInstance("TLS");    // 创建一个上下文（此处指定的协议类型似乎不是重点）
            X509TrustManager tm = new X509TrustManager() {     // 创建一个跳过SSL证书的策略
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }

                public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                }

                public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                }
            };
            ctx.init(null, new TrustManager[]{tm}, null);    // 使用上面的策略初始化上下文
            return new SSLConnectionSocketFactory(ctx, new String[]{"SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"}, null, NoopHostnameVerifier.INSTANCE);
        } catch (Exception e) {
            EsignDemoException ex = new EsignDemoException("忽略域名校验失败", e);
            ex.initCause(e);
            throw ex;
        }

    }

    /**
     * @return
     * @description 获取单例HttpClient
     * @author 澄泓
     */
    private static synchronized CloseableHttpClient getHttpClient() throws EsignDemoException {
        if (httpClient == null) {
            CredentialsProvider credsProvider = new BasicCredentialsProvider();
            credsProvider.setCredentials(new AuthScope(PROXY_IP, PROXY_PORT), new UsernamePasswordCredentials(PROXY_USERNAME, PROXY_PASSWORD));
            cfgPoolMgr();
            cfgRetryHandler();
            HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
            httpClient = httpClientBuilder.setDefaultCredentialsProvider(credsProvider).setConnectionManager(connMgr).setRetryHandler(retryHandler).build();
        }
        return httpClient;

    }
    //------------------------------私有方法end----------------------------------------------


}
